package cs236703.winter2013.hw4.solution;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import cs236703.winter2013.hw4.provided.JAssertionError;
import cs236703.winter2013.hw4.provided.JResult;
import cs236703.winter2013.hw4.provided.JResult.JTestResult;



public class JCore {
	public static void assertEquals(Object expected,Object actual) throws JAssertionError {
		if ((expected == null && actual == null) || (expected.equals(actual))) return;
		throw new JAssertionError(expected,actual);
	}
	
	// Checks if there's a method we the same name in the set. 
	// We need it since in case of a name conflict between @JTest functions we invoke the last one.
	private static boolean isMethodNotInSet(Method m, Set<Method> s) {
		for (Method m2 : s) {
			if (m.getName().equals(m2.getName())) return false;
		}
		return true;
	}
	
	private static void invokeAnyways(Object o, Method m) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		boolean accessible = m.isAccessible();
		m.setAccessible(true);
		m.invoke(o, (Object[]) null);
		m.setAccessible(accessible);
	}
	
	public static JTestSummary testClass(Class<?> classToTest) throws IllegalArgumentException {
		if (classToTest == null || !classToTest.isAnnotationPresent(JTestClass.class)) throw new IllegalArgumentException();

		Map<String,JResult> myMap = new HashMap<String,JResult>();
		
		// Errors Shoudln't rise, if it happens for some reason print the error on the console.
		try {
			// Create the object even if the constructor is a private one
			Constructor<?> constructor = classToTest.getDeclaredConstructor((Class<?>[])null);
	        constructor.setAccessible(true);
			Object myObj = constructor.newInstance((Object[]) null);
	
			// Create data structures to contain relevant methods
			Set<Method> JSetupMethods = new HashSet<Method>();
			Set<Method> JBeforeMethods = new HashSet<Method>();
			Map<Method, HashSet<Method>> JTestMethods = new HashMap<Method,HashSet<Method>>();
			
			// Iterate over the all methods in every level of the inheritance tree and put them in the right data structure.
			// We don't let a method with a name already found enter our data structure.
			Class<?> currClass = classToTest;
			while (currClass != null) {
				for (Method m: currClass.getDeclaredMethods()) {
					if (m.isAnnotationPresent(JSetup.class) && isMethodNotInSet(m,JSetupMethods)){
						JSetupMethods.add(m);
						continue;
					} 
					else if (m.isAnnotationPresent(JTest.class)&& isMethodNotInSet(m,JTestMethods.keySet())){
						JTestMethods.put(m,new HashSet<Method>());
						continue;
					} 
					else if (m.isAnnotationPresent(JBefore.class) && isMethodNotInSet(m,JBeforeMethods)){
						JBeforeMethods.add(m);
					}
				}
				currClass = currClass.getSuperclass();
			}
			
			
			// Once we found all the test methods we want to run, we iterate over the JBeforeMethods to update JTestMethods with the relevant methods
			for (Method m: JBeforeMethods) {
				JBefore a = m.getAnnotation(JBefore.class);
				for (String name : a.value()) {
					for (Entry<Method,HashSet<Method>> e: JTestMethods.entrySet()) {
						if (e.getKey().getName().equals(name)) {
							e.getValue().add(m);
							break;
						}
					}
				}
			}
			
			// Invoke JSetup Methods
			for (Method m: JSetupMethods) {
				invokeAnyways(myObj, m);
			}
			// Invoke JTest Methods
			for (Entry<Method,HashSet<Method>> e: JTestMethods.entrySet()) {
				// Invoke JBefore Methods
				for (Method m: e.getValue()) {
					invokeAnyways(myObj, m);
				}
				JResult result = new JResultClass(JTestResult.SUCCESS,null);
				try {
					invokeAnyways(myObj, e.getKey());
				}
				catch (InvocationTargetException exception) {
					if (exception.getTargetException() instanceof JAssertionError) {
						result = new JResultClass(JTestResult.FAILURE,exception.getTargetException().getMessage());
					} else {
						result = new JResultClass(JTestResult.ERROR,exception.getTargetException().getClass().getName());
					}
				}
				catch (Exception exception) {
					result =  new JResultClass(JTestResult.ERROR,exception.getClass().getName());
				}
				myMap.put(e.getKey().getName(), result);
			}
		}
		catch (Exception e) {
			System.out.println("Critical Error:" + e.getClass().getName() + " -> " + e.getMessage());
		}
		return new JTestSummary(myMap);
	}
}
